<!--
@license
Copyright (c) 2014 The Polymer Project Authors. All rights reserved.
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
Code distributed by Google as part of the polymer project is also
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
-->

<link rel="import" href="debounce.html">

<script>

  Polymer._collections = new WeakMap();

  Polymer.Collection = function(userArray) {
    Polymer._collections.set(userArray, this);
    this.userArray = userArray;
    this.store = userArray.slice();
    this.initMap();
  };

  Polymer.Collection.prototype = {

    constructor: Polymer.Collection,

    initMap: function() {
      var omap = this.omap = new WeakMap();
      var pmap = this.pmap = {};
      var s = this.store;
      for (var i=0; i<s.length; i++) {
        var item = s[i];
        if (item && typeof item == 'object') {
          omap.set(item, i);
        } else {
          pmap[item] = i;
        }
      }
    },

    add: function(item) {
      var key = this.store.push(item) - 1;
      if (item && typeof item == 'object') {
        this.omap.set(item, key);
      } else {
        this.pmap[item] = key;
      }
      return '#' + key;
    },

    removeKey: function(key) {
      if ((key = this._parseKey(key))) {
        this._removeFromMap(this.store[key]);
        delete this.store[key];
      }
    },

    _removeFromMap: function(item) {
      if (item && typeof item == 'object') {
        this.omap.delete(item);
      } else {
        delete this.pmap[item];
      }
    },

    remove: function(item) {
      var key = this.getKey(item);
      this.removeKey(key);
      return key;
    },

    getKey: function(item) {
      var key;
      if (item && typeof item == 'object') {
        key = this.omap.get(item);
      } else {
        key = this.pmap[item];
      }
      if (key != undefined) {
        return '#' + key;
      }
    },

    getKeys: function() {
      return Object.keys(this.store).map(function(key) {
        return '#' + key;
      });
    },

    _parseKey: function(key) {
      if (key && key[0] == '#') {
        return key.slice(1);
      }
    },

    setItem: function(key, item) {
      if ((key = this._parseKey(key))) {
        var old = this.store[key];
        if (old) {
          this._removeFromMap(old);
        }
        if (item && typeof item == 'object') {
          this.omap.set(item, key);
        } else {
          this.pmap[item] = key;
        }
        this.store[key] = item;
      }
    },

    getItem: function(key) {
      if ((key = this._parseKey(key))) {
        return this.store[key];
      }
    },

    getItems: function() {
      var items = [], store = this.store;
      for (var key in store) {
        items.push(store[key]);
      }
      return items;
    },

    // Accepts an array of standard splice records (index, addedCount, removed
    // array), and performs two key actions:
    // 1. Applies the splice to the collection: adds newly added items to the
    //    store which generates a unique key for it, and removes removed items
    //    (and their key) from the store
    // 2. Generates a "keySplices" record (in contrast to the input
    //    "indexSplices"), which contains an array of added and removed keys
    //    corresponding to the added/removed items
    _applySplices: function(splices) {
      // Dedupe added and removed keys to a final added/removed map
      var keyMap = {}, key;
      for (var i=0, s; (i<splices.length) && (s=splices[i]); i++) {
        s.addedKeys = [];
        for (var j=0; j<s.removed.length; j++) {
          key = this.getKey(s.removed[j]);
          keyMap[key] = keyMap[key] ? null : -1;
        }
        for (j=0; j<s.addedCount; j++) {
          var item = this.userArray[s.index + j];
          key = this.getKey(item);
          key = (key === undefined) ? this.add(item) : key;
          keyMap[key] = keyMap[key] ? null : 1;
          // Add an "addedKeys" array to indexSplices to capture keys associated
          // with added items, since references to added items can be lost by
          // further changes to the array by the time the splice is consumed
          s.addedKeys.push(key);
        }
      }
      // Convert added/removed key map to added/removed arrays
      var removed = [];
      var added = [];
      for (key in keyMap) {
        if (keyMap[key] < 0) {
          this.removeKey(key);
          removed.push(key);
        }
        if (keyMap[key] > 0) {
          added.push(key);
        }
      }
      return [{
        removed: removed,
        added: added
      }];
    }

  };

  Polymer.Collection.get = function(userArray) {
    return Polymer._collections.get(userArray) ||
      new Polymer.Collection(userArray);
  };

  Polymer.Collection.applySplices = function(userArray, splices) {
    // Only apply splices & generate keySplices if the array already has a
    // backing Collection, meaning there is an element monitoring its keys;
    // Splices that happen before the collection has been created must be
    // discarded to avoid double-entries
    var coll = Polymer._collections.get(userArray);
    return coll ? coll._applySplices(splices) : null;
  };

</script>
